Redux异步流
使用 middleware 简化异步请求
- redux-thunk
要发异步请求,在 Redux 定义中,最合适的位置是在 action creator 中实现。
Node.js 的 thunk 函数:
fs.readFile(fileName, callback)
var readFileThunk = Thunk(fileName)
readFileThunk(callback)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback)
}
}
Thunk 函数实现上就是针对多参数的 currying 以实现对函数的惰性求值。任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。redux-thunk 的源代码:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument)
}
return next(action)
}
}
我们很清楚地看到,当 action 为函数的时候,我们并没有调用 next 或 dispatch 方法,而是返回 action 的调用。这里的 action 即为一个 Thunk 函数,以达到将 dispatch 和 getState 参数传递到函数内的作用。
模拟请求一个天气的异步请求。action 通常可以这么写:
function getWeather(url, params) {
return (dispatch, getState) => {
fetch(url, params)
.then(result => {
dispatch({
type: 'GET_WEATHER_SUCCESS',
payload: result
})
})
.catch(err => {
dispatch({
type: 'GET_WEATHER_ERROR',
error: err
})
})
}
}
我们顺利地把同步 action 变成了异步 action。
尽管我们利用 Thunk 可以完成各种复杂的异步 action,但是对于某些复杂但是又有规律的场景,抽离出更合适的、目标更明确的 middleware 来解决会是更好的方案,而异步请求绝对是其一。
redux-promise
redux-promise middleware:
import { isFSA } from 'flux-standard-action'
function isPromise(val) {
return val && typeof val.then === 'function'
}
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action)
}
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true })
return Promise.reject(error)
}
)
: next(action)
}
}
redux-promise 兼容了 FSA 标准,也就是说将返回的结果保存在 payload 中。实现过程非常容易理解,即判断 action 或 action.payload 是否为 promise,如果是,就执行 then,返回的结果再发送一次 dispatch。
利用 ES7 的 async 和 await 语法,可以简化上述请求天气的异步过程:
const fetchData = (url, params) => fetch(url, params)
async function getWeather(url, params) {
const result = await fetchData(url, params)
if (result.error) {
return {
type: 'GET_WEATHER_ERROR',
error: result.error
}
}
return {
type: 'GET_WEATHER_SUCCESS',
payload: result
}
}
- redux-composable-fetch
在实际应用中,我们还需要加上 loading 状态。结合上述讨论的两个开源 middleware,我们完全可以自己实现一个更贴合工程需要的 middleware,这里将其命名为 redux-composable-fetch。
在理想情况下,我们不希望通过复杂的方法去请求数据,而希望通过如下形式一并完成在异步请求过程中的不同状态:
{
url: '/api/weather.json',
params: {
city: encodeURI(city)
},
types: ['GET_WEATHER', 'GET_WEATHER_SUCESS', 'GET_WEATHER_ERROR']
}
可以看到,异步请求 action 的格式有别于 FSA。它并没有使用 type 属性,而使用了 types 属性。types 其实是三个普通 action type 的集合,分别代表请求中、请求成功和请求失败。
在请求 middleware 中,会对 action 进行格式检查,若存在 url 和 types 属性,则说明这个action 是一个用于发送异步请求的 action。此外,并不是所有请求都能携带参数,因此 params 是可选的。
当请求 middleware 识别到这是一个用于发送请求的 action 后,首先会分发一个新的 action,这个 action 的 type 就是原 action 里 types 数组中的第一个元素,即请求中。分发这个新 action 的目的在于让 store 能够同步当前请求的状态,如将 loading 状态置为 true,这样在对应的界面上可以展示一个友好的加载中动画。
然后,请求 middleware 会根据 action 中的 url、params、method 等参数发送一个异步请求,并在请求响应后根据结果的成功或失败分别分发请求成功或请求失败的新 action。
请求 middleware 的简化实现如下,我们可以根据具体的场景对此进行改造:
const fetchMiddleware = store => next => action => {
if (!action.url || !Array.isArray(action.types)) {
return next(action)
}
const [LOADING, SUCCESS, ERROR] = action.types
next({
type: LOADING,
loading: true,
...action
})
fetch(action.url, { params: action.params })
.then(result => {
next({
type: SUCCESS,
loading: false,
payload: result
})
})
.catch(err => {
next({
type: ERROR,
loading: false,
error: err
})
})
}
这样我们的确一步就完成了异步请求的 action。
使用 middleware 处理复杂异步流
- 轮询
轮询是长连接的一种实现方式,它能够在一定时间内重新启动自身,然后再次发起请求。基于这个特性,我们可以在 redux-composable-fetch 的基础上再写一个 middleware,这里命名为redux-polling:
import setRafTimeout, { clearRafTimeout } from 'setRafTimeout'
export default ({ dispatch, getState }) =>
next =>
action => {
const { pollingUrl, params, types } = action
const isPollingAction = pollingUrl && params && types
if (!isPollingAction) {
return next(action)
}
let timeoutId = null
const startPolling = (timeout = 0) => {
timeoutId = setRafTimeout(() => {
const { pollingUrl, ...others } = action
const pollingAction = {
...others,
url: pollingUrl,
timeoutId
}
dispatch(pollingAction).then(data => {
if (data && data.interval && typeof data.interval === 'number') {
startPolling(data.interval * 1000)
} else {
console.error('pollingAction should fetch data contain interval')
}
})
}, timeout)
}
startPolling()
}
export const clearPollingTimeout = timeoutId => {
if (timeoutId) {
clearRafTimeout(timeoutId)
}
}
在这个 middleware 的实现中,我们用到了 raf 函数,在 2.7 节中我们已经提到过它。raf 是实现中的关键点之一,它可以让请求在一定时间内重新发起。
另外,在 API 的设计上,我们还暴露了 clearPollingTimeout 方法,以便我们在需要时手动停止轮询。
最后,调用 action 来发起轮询:
{
pollingUrl: '/api/weather.json',
params: {
city: encodeURI(city),
},
types: [null, 'GET_WEATHER_SUCESS', null],
}
- 多异步串联
多异步串联是我们在应用场景中常见的逻辑,根据以往的经验,是不是很快就想到用 promise 去实现。
我们试想,通过对 promise 封装是不是能够做到不论是否是异步请求,都通过 promise 来传递以达到一个统一的效果。的确,这一点非常容易就可以实现:
const sequenceMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (!Array.isArray(action)) {
return next(action)
}
return action.reduce((result, currAction) => {
return result.then(() => {
return Array.isArray(currAction) ? Promise.all(currAction.map(item => dispatch(item))) : dispatch(currAction)
})
}, Promise.resolve())
}
这里我们定义了一个名为 sequenceMiddleware 的 middleware。在构建 action creator 时,会传递一个数组,数组中每一个值都将是按顺序执行的步骤。这里的步骤既可以是异步的,也可以是同步的。
在实现过程中,我们非常巧妙地使用 Promise.resolve() 来初始化 action.reduce 方法,然后始终使用 Promise.then() 方法串联起数组,达到了串联步骤的目的。
这里还是使用之前的例子。假设我们的应用初始化时会先获取当前城市,然后获取当前城市的天气信息,那么就可以这么写:
function getCurrCity(ip) {
return {
url: '/api/getCurrCity.json',
params: { ip },
types: [null, 'GET_CITY_SUCCESS', null]
}
}
function getWeather(cityId) {
return {
url: '/api/getWeatherInfo.json',
params: { cityId },
types: [null, 'GET_WEATHER_SUCCESS', null]
}
}
function loadInitData(ip) {
return [
getCurrCity(ip),
(dispatch, state) => {
dispatch(getWeather(getCityIdWithState(state)))
}
]
}
这种方法利用了数组的特性。可以看到,它已经覆盖了大部分场景。当然,如果串联过程中有不同的分支,就无能为力了。
- redux-saga
在 Redux 社区,还有一个处理异步流的后起之秀,名为 redux-saga。它与上述方法最直观的不同就是用 generator 替代了 promise,我们通过 Babel 可以很方便地支持 generator:
function* getCurrCity(ip) {
const data = yield call('/api/getCurrCity.json', { ip });
yield put({
type: 'GET_CITY_SUCCESS',
payload: data,
});
}
function* getWeather(cityId) {
const data = yield call('/api/getWeatherInfo.json', { cityId });
yield put({
type: 'GET_WEATHER_SUCCESS',
payload: data,
});
}
function loadInitData(ip) {
yield getCurrCity(ip);
yield getWeather(getCityIdWithState(state));
yield put({
type: 'GET_DATA_SUCCESS',
});
}
redux-saga 的确是最优雅的通用解决方案,它有着灵活而强大的协程机制,可以解决任何复杂的异步交互。